#!/usr/bin/python2.4
#
# Copyright 2008 Google Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.


"""spiral's UI module.

spiral aims to be a faster and nicer packages.debian.org replacement.
"""

__author__ = 'stratus@google.com (Gustavo Franco)'

import logging
import operator
import os
import string
import time
import wsgiref.handlers
from google.appengine.api import memcache
from google.appengine.api import users
from google.appengine.ext import search
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
import ranking


class BaseRequestHandler(webapp.RequestHandler):
  """Supplies a common template generation function."""

  def generate(self, template_name, template_values=None):
    """Overrides generate webapp.RequestHandler method.

    This generate overrides the original one to set user and request variables
    in a way they can be used in the django templates (templates/ subdirectory).

    Args:
      template_name: a string containing a template name (ie: index.html).
      template_values: a dict of values to be set.
    """
    values = {
        'user': users.get_current_user(),
        'request': self.request,
    }

    directory = os.path.dirname(__file__)
    path = os.path.join(directory, os.path.join('templates', template_name))

    if template_values:
      values.update(template_values)

    self.response.out.write(template.render(path, values, debug=True))


class SpiralPage(BaseRequestHandler):
  """Acts as the spiral's main page dispatcher."""

  def get(self):
    """Parses spiral's main page GET requests.

    When you call get() and there is a keyword set, it will use that to
    perform a search and call generate to show you the resulting page,
    otherwise it will call generate to show the main page containing
    just a form.
    """
    keywords = self.request.get('keywords')
    if keywords:
      logging.info('keywords: %s' % keywords)

      # 62 is arbitrary but we need a cap to avoid blow the CPU quota.
      if len(keywords) > 62:
        logging.error('keywords %s length is greater than 62, search skipped')
        keywords = None
        self.generate('index.html')
      else:
        pkgsearch = Search()
        result = pkgsearch.get(string.strip(keywords))
        self.generate('index.html', {'keywords': keywords, 'result': result})
    else:
      # catch URIs containing /keyword
      urlkeyword = self.request.uri.split('/')[-1]
      if urlkeyword:
        self.redirect('?keywords=' + urlkeyword)
      else:
        self.generate('index.html')


class Search(webapp.RequestHandler):
  """Spiral's search frontend."""

  def get(self, keywords):
    """Parses search frontend GET requests.

    It calls the Google App Engine search mechanism and perform a keyword based
    query.

    Args:
      keywords: a string to be used in search.

    Returns:
      A list with information about 10 most relevant packages maximum.
    """
    start = time.time()
    cached_results = memcache.get(keywords)
    stats = memcache.get_stats()
    if stats:
      logging.debug('Cache hits: %s', stats['hits'])
      logging.debug('Cache misses: %s', stats['misses'])

    if cached_results:
      logging.debug('Cache hit by %s', keywords)
      response_time = time.time() - start
      logging.debug('Response time for cached result: %.4f' % response_time)
      # Returns 10 results only otherwise Google App Engine quota may kill
      # Spiral.
      return cached_results[:10]
    else:
      # PackageX must match bulkload's module kind as well. It is PackageX, due
      # Google App Engine limitations. This code follows advice by ryanb.
      query = search.SearchableQuery('PackageLenny1')
      query.Search(keywords)
      packages = []
      for package in query.Run():
        packages.append(package)

      rank = ranking.Ranking(keywords)
      packages.sort(cmp=rank.SortBySubstring, key=operator.itemgetter('name'))
      packages.sort(cmp=rank.SortByVotes, key=operator.itemgetter('votes'))
      packages.sort(cmp=rank.SortByExactmatch, key=operator.itemgetter('name'))

      try:
        memcache.add(keywords, packages, 3600)
      except ValueError, message:
        logging.error(message)

    responsetime = time.time() - start
    logging.debug('Response time: %.4f' % responsetime)

    # Returns 10 results only otherwise Google App Engine quota may kill Spiral.
    return packages[:10]


def main():
  application = webapp.WSGIApplication([('/.*', SpiralPage)], debug=False)
  wsgiref.handlers.CGIHandler().run(application)


if __name__ == '__main__':
  main()
